Imports System.IO
Imports Microsoft.DirectX
Imports Microsoft.DirectX.DirectSound
Imports System.Threading

''' <summary>
''' Represents a sound recorder capable of saving captured sound information to disk.
''' </summary>
Public Class SoundRecorder
    Implements IDisposable
    ' Number of notifications that DirectSound will provide us while filling up a sound buffer.
    Private Const NumberRecordNotifications As Integer = 16
    Private Const MicrophoneSearch As String = "microphone"

    ' Device type that will be used.
    Private _desiredDeviceType As SoundDeviceType = SoundDeviceType.Default
    ' Available capture devices to search through and select from.
    Private _devices As CaptureDevicesCollection
    ' Index into CaptureDevicesCollection where _desiredDeviceType found.
    Private _selectedDevice As Integer = 0

    ' Constructed sound format for the desired device.
    Private _recorderFormat As SoundFormat = Nothing

    ' DirectSound Capture object specific to the desired device.
    Private _applicationDevice As Capture = Nothing
    ' Buffer used by DirectSound to record sound data.
    Private _applicationBuffer As CaptureBuffer = Nothing

    Private _sampleCount As Integer = 0
    Private _captureBufferSize As Integer = 0

    Private _fileName As String = String.Empty
    Private _waveFile As FileStream = Nothing
    Private _writer As BinaryWriter = Nothing

    Private _capturing As Boolean = False
    Private _recording As Boolean = False

    ' Tied to WaitThread method, responsible for saving buffered samples when signaled.
    Private _notifyThread As Thread = Nothing
    ' Event that is singaled by DirectSound to indicate data available.
    Private _notificationEvent As AutoResetEvent = Nothing
    ' Allows DirectSound to know what capture buffer and what positions within that buffer
    ' are set to have notifications.
    Private _applicationNotify As Notify = Nothing
    Private _notifySize As Integer = 0
    Private _nextCaptureOffset As Integer = 0
    ' Will contain offsets that represent positions within the capture buffer for
    ' notifications.
    Private _positionNotify As BufferPositionNotify() = New BufferPositionNotify(NumberRecordNotifications) {}

    ''' <summary>
    ''' Create a new sound recorder.
    ''' </summary>
    ''' <param name="type">Sound capture device.</param>
    ''' <param name="rate">Desired sample rate.</param>
    ''' <param name="size">Desired sample size.</param>
    ''' <param name="channels">Desired channels to use.</param>
    Public Sub New(ByVal type As SoundDeviceType, ByVal rate As SampleRate, ByVal size As SampleSize, ByVal channels As Short)
        Me._desiredDeviceType = type
        Me._devices = New CaptureDevicesCollection()

        If Me._devices Is Nothing OrElse Me._devices.Count < 1 Then
            Throw New InvalidOperationException("No sound capture devices detected.")
        End If

        Me.Find(type)

        InitDirectSound()

        Me._recorderFormat = New SoundFormat(Me._applicationDevice, rate, size, channels)
    End Sub

    ''' <summary>
    ''' Start recording.
    ''' </summary>
    ''' <param name="filename">Filename to record sound to.</param>
    Public Sub Start(ByVal filename As String)
        If Nothing Is filename Then
            Throw New ArgumentNullException(filename)
        End If

        Me._fileName = filename

        Me.CreateSoundFile()

        ' Create a capture buffer, and tell the capture 
        ' buffer to start recording   
        CreateCaptureBuffer()

        InitNotifications()

        Me._applicationBuffer.Start(True)
    End Sub

    ''' <summary>
    ''' Stop recording.
    ''' </summary>
    Public Sub [Stop]()
        ' Stop the buffer, and read any data that was not 
        ' caught by a notification
        Me._applicationBuffer.Stop()

        RecordCapturedData()

        Me._writer.Seek(4, SeekOrigin.Begin) ' Seek to the length descriptor of the RIFF file.
        Me._writer.Write(CInt(Fix(Me._sampleCount + 36))) ' Write the file length, minus first 8 bytes of RIFF description.
        Me._writer.Seek(40, SeekOrigin.Begin) ' Seek to the data length descriptor of the RIFF file.
        Me._writer.Write(Me._sampleCount) ' Write the length of the sample data in bytes.

        Me._writer.Close() ' Close the file now.
        Me._writer = Nothing ' Set the writer to null.
        Me._waveFile = Nothing ' Set the FileStream to null.

        Me._fileName = String.Empty
    End Sub

    ''' <summary>
    ''' Get the GUID for the specified sound device type.
    ''' </summary>
    ''' <param name="type">Sound capture device type to select.</param>
    ''' <returns>GUID of selected sound device type.</returns>
    Private Function GetDeviceGuid(ByVal type As SoundDeviceType) As Guid
        Find(type)

        Return Me._devices(Me._selectedDevice).DriverGuid
    End Function

    Private Sub Find(ByVal type As SoundDeviceType)
        Select Case type
            Case SoundDeviceType.Default
                Me._selectedDevice = 0
            Case SoundDeviceType.Microphone
                Me._selectedDevice = FindMicrophone()
        End Select
    End Sub

    Private Function FindMicrophone() As Integer
        Dim microphone As Integer = 0

        For i As Integer = 0 To Me._devices.Count - 1
            If Me._devices(i).Description.ToLower().Contains(MicrophoneSearch) Then
                microphone = i
                Exit For
            End If
        Next i

        Return microphone
    End Function

    Private Sub InitDirectSound()
        Me._captureBufferSize = 0
        Me._notifySize = 0

        ' Create DirectSound.Capture using the preferred capture device.
        Me._applicationDevice = New Capture(GetDeviceGuid(Me._desiredDeviceType))
    End Sub

    Private Sub CreateSoundFile()
        '-----------------------------------------------------------------------------
        ' Name: OnCreateSoundFile()
        ' Desc: Called when the user requests to save to a sound file
        '-----------------------------------------------------------------------------
        If Me._recording Then
            ' Stop the capture and read any data that 
            ' was not caught by a notification
            Me.Stop()
            Me._recording = False
        End If

        Try
            CreateRIFF()
        Catch
            Throw
        End Try
    End Sub

    Private Sub CreateRIFF()
        '*************************************************************************

        'Here is where the file will be created. A
        'wave file is a RIFF file, which has chunks
        'of data that describe what the file contains.
        'A wave RIFF file is put together like this:

        'The 12 byte RIFF chunk is constructed like this:
        'Bytes 0 - 3 :	'R' 'I' 'F' 'F'
        'Bytes 4 - 7 :	Length of file, minus the first 8 bytes of the RIFF description.
        '(4 bytes for "WAVE" + 24 bytes for format chunk length +
        '8 bytes for data chunk description + actual sample data size.)
        'Bytes 8 - 11:	'W' 'A' 'V' 'E'

        'The 24 byte FORMAT chunk is constructed like this:
        'Bytes 0 - 3 :	'f' 'm' 't' ' '
        'Bytes 4 - 7 :	The format chunk length. This is always 16.
        'Bytes 8 - 9 :	File padding. Always 1.
        'Bytes 10- 11:	Number of channels. Either 1 for mono,  or 2 for stereo.
        'Bytes 12- 15:	Sample rate.
        'Bytes 16- 19:	Number of bytes per second.
        'Bytes 20- 21:	Bytes per sample. 1 for 8 bit mono, 2 for 8 bit stereo or
        '16 bit mono, 4 for 16 bit stereo.
        'Bytes 22- 23:	Number of bits per sample.

        'The DATA chunk is constructed like this:
        'Bytes 0 - 3 :	'd' 'a' 't' 'a'
        'Bytes 4 - 7 :	Length of data, in bytes.
        'Bytes 8 -...:	Actual sample data.

        '**************************************************************************

        ' Open up the wave file for writing.
        Me._waveFile = New FileStream(Me._fileName, FileMode.Create)
        Me._writer = New BinaryWriter(Me._waveFile)

        ' Set up file with RIFF chunk info.
        Dim ChunkRiff As Char() = {"R"c, "I"c, "F"c, "F"c}
        Dim ChunkType As Char() = {"W"c, "A"c, "V"c, "E"c}
        Dim ChunkFmt As Char() = {"f"c, "m"c, "t"c, " "c}
        Dim ChunkData As Char() = {"d"c, "a"c, "t"c, "a"c}

        Dim shPad As Short = 1 ' File padding
        Dim nFormatChunkLength As Integer = &H10 ' Format chunk length.
        Dim nLength As Integer = 0 ' File length, minus first 8 bytes of RIFF description. This will be filled in later.
        Dim shBytesPerSample As Short = 0 ' Bytes per sample.

        ' Figure out how many bytes there will be per sample.
        If 8 = Me._recorderFormat.Format.BitsPerSample AndAlso 1 = Me._recorderFormat.Format.Channels Then
            shBytesPerSample = 1
        ElseIf (8 = Me._recorderFormat.Format.BitsPerSample AndAlso 2 = Me._recorderFormat.Format.Channels) OrElse (16 = Me._recorderFormat.Format.BitsPerSample AndAlso 1 = Me._recorderFormat.Format.Channels) Then
            shBytesPerSample = 2
        ElseIf 16 = Me._recorderFormat.Format.BitsPerSample AndAlso 2 = Me._recorderFormat.Format.Channels Then
            shBytesPerSample = 4
        End If

        ' Fill in the riff info for the wave file.
        Me._writer.Write(ChunkRiff)
        Me._writer.Write(nLength)
        Me._writer.Write(ChunkType)

        ' Fill in the format info for the wave file.
        Me._writer.Write(ChunkFmt)
        Me._writer.Write(nFormatChunkLength)
        Me._writer.Write(shPad)
        Me._writer.Write(Me._recorderFormat.Format.Channels)
        Me._writer.Write(Me._recorderFormat.Format.SamplesPerSecond)
        Me._writer.Write(Me._recorderFormat.Format.AverageBytesPerSecond)
        Me._writer.Write(shBytesPerSample)
        Me._writer.Write(Me._recorderFormat.Format.BitsPerSample)

        ' Now fill in the data chunk.
        Me._writer.Write(ChunkData)
        Me._writer.Write(CInt(Fix(0))) ' The sample length will be written in later.
    End Sub

    Private Sub CreateCaptureBuffer()
        Dim dscheckboxd As CaptureBufferDescription = New CaptureBufferDescription()

        If Not Nothing Is Me._applicationNotify Then
            Me._applicationNotify.Dispose()
            Me._applicationNotify = Nothing
        End If
        If Not Nothing Is Me._applicationBuffer Then
            Me._applicationBuffer.Dispose()
            Me._applicationBuffer = Nothing
        End If

        If 0 = Me._recorderFormat.Format.Channels Then
            Return
        End If

        ' Set the notification size
        If (1024 > Me._recorderFormat.Format.AverageBytesPerSecond / 8) Then
            Me._notifySize = 1024
        Else
            Me._notifySize = (Me._recorderFormat.Format.AverageBytesPerSecond / 8)
        End If
        Me._notifySize -= _notifySize Mod Me._recorderFormat.Format.BlockAlign

        ' Set the buffer sizes
        Me._captureBufferSize = Me._notifySize * NumberRecordNotifications

        ' Create the capture buffer
        dscheckboxd.BufferBytes = Me._captureBufferSize
        'this._recorderFormat.Format.FormatTag = WaveFormatTag.Pcm;
        dscheckboxd.Format = Me._recorderFormat.Format ' Set the format during creatation

        Me._applicationBuffer = New CaptureBuffer(dscheckboxd, Me._applicationDevice)
        Me._nextCaptureOffset = 0

        'InitNotifications();
    End Sub

    Private Sub InitNotifications()
        If Nothing Is Me._applicationBuffer Then
            Throw New NullReferenceException()
        End If

        ' Create a thread to monitor the notify events
        If Nothing Is Me._notifyThread Then
            ' Create a notification event
            Me._notificationEvent = New AutoResetEvent(False)

            Me._notifyThread = New Thread(AddressOf WaitThread)
            Me._capturing = True
            Me._notifyThread.Start()
        End If

        ' Setup the notification positions
        For i As Integer = 0 To NumberRecordNotifications - 1
            Me._positionNotify(i).Offset = (Me._notifySize * i) + Me._notifySize - 1
            Me._positionNotify(i).EventNotifyHandle = Me._notificationEvent.Handle
        Next i

        ' Setup DirectSound Notify object to operate on our capture buffer.
        Me._applicationNotify = New Notify(Me._applicationBuffer)

        ' Tell DirectSound when to notify the app. The notification will come in the form 
        ' of signaled events that are handled in the notify thread.
        Me._applicationNotify.SetNotificationPositions(Me._positionNotify, NumberRecordNotifications)
    End Sub

    Private Sub RecordCapturedData()
        '-----------------------------------------------------------------------------
        ' Name: RecordCapturedData()
        ' Desc: Copies data from the capture buffer to the output buffer 
        '-----------------------------------------------------------------------------
        Dim CaptureData As Byte() = Nothing
        Dim ReadPos As Integer
        Dim CapturePos As Integer
        Dim LockSize As Integer

        Me._applicationBuffer.GetCurrentPosition(CapturePos, ReadPos)

        LockSize = ReadPos - Me._nextCaptureOffset
        If LockSize < 0 Then
            LockSize += Me._captureBufferSize
        End If

        ' Block align lock size so that we are always write on a boundary
        LockSize -= (LockSize Mod Me._notifySize)

        If 0 = LockSize Then
            Return
        End If

        ' Read the capture buffer.
        CaptureData = CType(Me._applicationBuffer.Read(Me._nextCaptureOffset, GetType(Byte), LockFlag.None, LockSize), Byte())

        ' Write the data into the wav file
        Me._writer.Write(CaptureData, 0, CaptureData.Length)

        ' Update the number of samples, in bytes, of the file so far.
        Me._sampleCount += CaptureData.Length

        ' Move the capture offset along
        Me._nextCaptureOffset += CaptureData.Length
        Me._nextCaptureOffset = Me._nextCaptureOffset Mod Me._captureBufferSize ' Circular buffer
    End Sub

    ' Note: runs on different thread.
    Private Sub WaitThread()
        Do While Me._capturing
            'Sit here and wait for a message to arrive
            Me._notificationEvent.WaitOne(Timeout.Infinite, True)
            RecordCapturedData()
        Loop
    End Sub

#Region "IDisposable Members"

    Public Sub Dispose() Implements IDisposable.Dispose
        ShutDown()
    End Sub

    Private Sub ShutDown()
        If Not Nothing Is Me._notificationEvent Then
            Me._capturing = False
            Me._notificationEvent.Set()
        End If
        If Not Nothing Is Me._applicationBuffer Then
            If Me._applicationBuffer.Capturing Then
                Me.Stop()
            End If
        End If
    End Sub

#End Region

End Class
